package com.misoneism.fire.service
{
	import com.adobe.utils.StringUtil;
	import com.google.maps.LatLng;
	import com.google.maps.interfaces.IClientGeocoder;
	import com.google.maps.services.GeocodingEvent;
	import com.misoneism.fire.event.IncidentParserEvent;
	import com.misoneism.fire.model.CFAIncident;
	import com.misoneism.fire.model.IncidentSizeEnum;
	import com.misoneism.fire.model.IncidentStatusEnum;
	import com.misoneism.fire.model.IncidentTypeEnum;
	
	import flash.events.EventDispatcher;
	
	import org.as3commons.collections.ArrayList;
	import org.as3commons.collections.framework.IList;
	
	
	/**
	 * Parser used for processing a CFA incident feed.
	 */
	public class CFAIncidentParser extends EventDispatcher implements IIncidentParser
	{
		private static const RSS_TITLE: String = "Statewide Current Incident Summary";
		private static const DESCRIPTION_SEPARATOR: String = ": ";
		private static const BASE_GEOCODING_LOCATION: String = " victoria australia";
		
		private static const SIZE_MAP: Object =
			{
				"SPOT": IncidentSizeEnum.SPOT,
				"SMALL": IncidentSizeEnum.SMALL,
				"MEDIUM": IncidentSizeEnum.MEDIUM,
				"LARGE": IncidentSizeEnum.LARGE
			};
		
		private static const STATUS_MAP: Object =
			{
				"SAFE": IncidentStatusEnum.SAFE,
				"GOING": IncidentStatusEnum.GOING,
				"CONTROLLED": IncidentStatusEnum.CONTROLLED,
				"CONTAINED": IncidentStatusEnum.CONTAINED,
				"COMPLETED": IncidentStatusEnum.COMPLETED
			};
		
		private static const TYPE_MAP: Object =
			{
				"FALSE ALARM": IncidentTypeEnum.FALSE_ALARM,
				"STRUCTURE": IncidentTypeEnum.STRUCTURE,
				"NON STRUCTURE": IncidentTypeEnum.NON_STRUCTURE,
				"OTHER": IncidentTypeEnum.OTHER,
				"INCIDENT": IncidentTypeEnum.INCIDENT,
				"HAZMAT INCIDENT": IncidentTypeEnum.HAZMAT_INCIDENT,
				"GRASS": IncidentTypeEnum.GRASS,
				"WILD FIRE": IncidentTypeEnum.WILD_FIRE,
				"RESCUE": IncidentTypeEnum.RESCUE,
				"SCRUB": IncidentTypeEnum.SCRUB,
				"FOREST": IncidentTypeEnum.FOREST,
				"WASHAWAY": IncidentTypeEnum.WASHAWAY
			};
		
		
		/** Geocoder used to translate addresses into coordinates. */
		[Inject]
		public var clientGeocoder: IClientGeocoder;
		
		/** Queue of items to be processed and have their locations geocoded. */
		private var _processingQueue: ArrayList /* of CFAIncident */;
		
		/** Are we currently geocoding items? */
		private var _processingActive: Boolean;
		
		/** Index of the item currently being processed for geocoding. */
		private var _processingIndex: uint;
		
		public function CFAIncidentParser(  )
		{
			_processingQueue = new ArrayList(  );
			_processingActive = false;
		}
		
		
		/**
		 * Called after all dependencies have been injected.
		 */
		[PostConstruct]
		public function postConstruct(  ): void
		{
			clientGeocoder.addEventListener( GeocodingEvent.GEOCODING_SUCCESS, onGeocodingSuccess );
			clientGeocoder.addEventListener( GeocodingEvent.GEOCODING_FAILURE, onGeocodingFailure );
		}
		
		
		/**
		 * @inheritDoc
		 */
		public function parseXML( xml: XML ): void
		{
			// Do some basic format checking
			if ( ( xml.name(  ) != "rss" ) || ( !xml.hasOwnProperty( "channel" ) ) || ( !xml.channel.hasOwnProperty( "title" ) ) ||
					( xml.channel.title.toString(  ) != RSS_TITLE ) )
			{
				throw new InvalidFormatError(  );
			}
			
			var incidents: IList = new ArrayList(  );
			
			// Iterate over each of the incidents, parsing them
			for each ( var incidentXml: XML in xml.channel.item )
			{
				var rssGuid: uint = uint( incidentXml.guid );
				
				// Extract the description, and strip out any HTML content
				var description: String = String( incidentXml.description ).replace( /<b>/g, "" ).
					replace( /<\/b>/g, "" ).replace( /<br>/g, "" );
				var descriptionParts: Array /* of String */ = description.split( "\n" );
				
				// Ensure we have the correct data
				if ( descriptionParts.length != 9 )
				{
					throw new InvalidFormatError(  );
				}
				
				var suburb: String = getDescriptionItem( descriptionParts, 1 );
				var street: String = getDescriptionItem( descriptionParts, 2 );
				var whenUpdated: Date = parseDate( getDescriptionItem( descriptionParts, 3 ) );
				var whenCreated: Date = parseDate( getDescriptionItem( descriptionParts, 8 ) );
				var appliances: uint = uint( getDescriptionItem( descriptionParts, 7 ) );
				var status: IncidentStatusEnum = parseStatus( getDescriptionItem( descriptionParts, 5 ) );
				var size: IncidentSizeEnum = parseSize( getDescriptionItem( descriptionParts, 6 ) );
				var type: IncidentTypeEnum = parseType( getDescriptionItem( descriptionParts, 4 ) );
				
				incidents.add( new CFAIncident( rssGuid, street, suburb, appliances, whenUpdated,
					whenCreated, status, size, type ) );
			}
			
			// Add all these items onto the queue to be processed
			_processingQueue.addAllAt( _processingQueue.size, incidents.toArray(  ) );
			geocodeItems(  );
		}
		
		
		/**
		 * Request that items in the processing queue be geocoded.
		 */
		private function geocodeItems(  ): void
		{
			if ( !_processingActive )
			{
				_processingActive = true;
				_processingIndex = 0;
				
				generateGeocodeRequest(  );
			}
		}
		
		
		/**
		 * Generate a geocode request.
		 */
		private function generateGeocodeRequest(  ): void
		{
			var cfaIncident: CFAIncident = _processingQueue.itemAt( _processingIndex );
			
			clientGeocoder.geocode( ( ( cfaIncident.street != null ) ? cfaIncident.street + " " : "" ) + cfaIncident.suburb + BASE_GEOCODING_LOCATION );
		}
		
		
		/**
		 * Callback for geocoding succeeding.
		 */
		private function onGeocodingSuccess( event: GeocodingEvent ): void
		{
			var location: LatLng = event.response.placemarks[0].point as LatLng;
			var cfaIncident: CFAIncident = _processingQueue.itemAt( _processingIndex );
			
			// Add the geocoded location to the incident the request came from
			cfaIncident.location = location;
			
			// Move on to the next incident
			advanceItem(  );
		}
		
		
		/**
		 * Callback for geocoding failing.
		 */
		private function onGeocodingFailure( event: GeocodingEvent ): void
		{
			advanceItem(  );
		}
		
		
		private function advanceItem(  ): void
		{
			_processingIndex++;
			if ( _processingIndex < _processingQueue.size )
			{
				generateGeocodeRequest(  );
			}
			else
			{	// If there are no incidents left, mark that we're done and dispatch all the incidents off in an event
				_processingActive = false;
				
				dispatchEvent( new IncidentParserEvent( IncidentParserEvent.COMPLETED, false, false, _processingQueue ) );
				
				_processingQueue = new ArrayList(  );
			}
		}
		
		
		/**
		 * Extract a description element from the description section using the specified index.
		 */
		private function getDescriptionItem( descriptionParts: Array /* of String */, index: uint ): String
		{
			return StringUtil.trim( descriptionParts[ index ].substr(
				descriptionParts[ index ].indexOf( DESCRIPTION_SEPARATOR ) + DESCRIPTION_SEPARATOR.length ) );
		}
		
		
		/**
		 * Parse the status description into an appropriate enum.
		 */
		private function parseStatus( status: String ): IncidentStatusEnum
		{
			if ( STATUS_MAP.hasOwnProperty( status ) )
			{
				return STATUS_MAP[ status ];
			}
			else
			{
				return IncidentStatusEnum.UNKNOWN;
			}
		}
		
		
		/**
		 * Parse the type description into an appropriate enum.
		 */
		private function parseType( type: String ): IncidentTypeEnum
		{
			if ( TYPE_MAP.hasOwnProperty( type ) )
			{
				return TYPE_MAP[ type ];
			}
			else
			{
				return IncidentTypeEnum.UNKNOWN;
			}
		}
		
		
		/**
		 * Parse the size description into an appropriate enum.
		 */
		private function parseSize( size: String ): IncidentSizeEnum
		{
			if ( SIZE_MAP.hasOwnProperty( size ) )
			{
				return SIZE_MAP[ size ];
			}
			else // We default to SMALL
			{
				return IncidentSizeEnum.SMALL;
			}
		}
		
		
		/**
		 * Parse the textual representation of a date into a AS3 Date.
		 * 
		 * The date is in the following format:
		 * DD/MM/YY hh:mm:ss AMPM
		 * 
		 * e.g.
		 * 18/07/11 04:34:00 PM
		 */
		private function parseDate( date: String ): Date
		{
			return new Date( 2000 + uint( date.substr( 6, 2 ) ), uint( date.substr( 3, 2 ) )-1, uint( date.substr( 0, 2 ) ),
				( ( date.substr( 18, 2 ) == "PM" && uint( date.substr( 9, 2 ) ) != 12 ) ? 12 : 0 ) + uint( date.substr( 9, 2 ) ), uint( date.substr( 12, 2 ) ),
				uint( date.substr( 15, 2 ) ) );
		}
	}
}